﻿namespace NTLib.Core.Configuration
{
    using NTLib.Core.Reflection;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Threading;

    /// <summary>
    /// Configuration manager used to configure an instance using the configuration attribute mecanisme.
    /// </summary>
    public static class ConfigManager
    {
        #region Fields

        private static readonly SemaphoreLocker s_locker;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes the <see cref="ConfigManager"/> class.
        /// </summary>
        static ConfigManager()
        {
            s_locker = new SemaphoreLocker();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Configures an instance using the attribute configuration mecanisme.
        /// </summary>
        /// <typeparam name="T">IConfigurable type</typeparam>
        /// <param name="configInstance">The configuration instance.</param>
        /// <param name="context">The context.</param>
        /// <param name="token">The token.</param>
        /// <exception cref="System.ArgumentNullException">Exception raised if the argument pass in parameters is null</exception>
        /// <exception cref="NTLib.Core.Exceptions.Configuration.ConfigurationException">Exception raised by the configuration attribute if any error occured during the configuration process</exception>
        public static void Configure<T>(T configInstance, object context = null, CancellationToken token = default(CancellationToken))
            where T : IConfigurable
        {
            Task t = ConfigureAsync(configInstance, context, token);
            t.Wait(token);
        }

        /// <summary>
        /// Configures an instance using the attribute configuration mecanisme.
        /// </summary>
        /// <typeparam name="T">IConfigurable type</typeparam>
        /// <param name="configInstance">The configuration instance.</param>
        /// <param name="context">The context.</param>
        /// <param name="token">The token.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.ArgumentNullException">Exception raised if the argument pass in parameters is null</exception>
        /// <exception cref="NTLib.Core.Exceptions.Configuration.ConfigurationException">Exception raised by the configuration attribute if any error occured during the configuration process</exception>
        public static async Task ConfigureAsync<T>(T configInstance, object context = null, CancellationToken token = default(CancellationToken))
            where T : IConfigurable
        {
            if (configInstance == null)
                throw new ArgumentNullException(nameof(configInstance));

            token.ThrowIfCancellationRequested();
            using (await s_locker.ScopeLockAsync(false, token))
            {

                if (configInstance.IsConfigured)
                    return;

                token.ThrowIfCancellationRequested();

                var host = configInstance.GetType();
                token.ThrowIfCancellationRequested();

                var attrs = ExtractConfigurationAttributes(host);
                token.ThrowIfCancellationRequested();

                var data = ExtractValuesFromConfigurationAttributes(attrs, host, context, token);
                token.ThrowIfCancellationRequested();

                configInstance.LoadConfig(data);
            }
        }

        /// <summary>
        /// Extracts the configuration attributes.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private static IEnumerable<BaseConfigurationAttribute> ExtractConfigurationAttributes(Type type)
        {
            List<BaseConfigurationAttribute> attrs = new List<BaseConfigurationAttribute>();
            Dictionary<Type, BaseConfigurationAttribute> attrsDico = new Dictionary<Type, BaseConfigurationAttribute>();

            while (type != null)
            {
                var attributes = type.GetTypeInfo().GetCustomAttributes(false);

                if (attributes != null && attributes.Any())
                {
                    foreach (var attr in attributes)
                    {
                        BaseConfigurationAttribute confAttr = attr as BaseConfigurationAttribute;

                        if (confAttr != null)
                        {
                            var trueType = confAttr.GetType();
                            if (confAttr.OverrideAllTheOther)
                            {
                                attrsDico.Clear();
                                attrsDico.Add(trueType, confAttr);
                                attrs.Clear();
                                attrs.Add(confAttr);
                                break;
                            }

                            BaseConfigurationAttribute previous = null;
                            if (attrsDico.TryGetValue(trueType, out previous))
                            {
                                if (previous.OverrideParent)
                                    continue;
                                attrsDico[trueType] = confAttr;
                            }
                            else
                            {
                                attrsDico.Add(trueType, confAttr);
                            }

                            attrs.Add(confAttr);
                        }
                    }
                }

                type = type.GetTypeInfo().BaseType;
            }

            return attrs;
        }

        /// <summary>
        /// Extracts the values from configuration attributes.
        /// </summary>
        /// <param name="attrs">The attrs.</param>
        /// <param name="host">The host.</param>
        /// <param name="context">The context.</param>
        /// <param name="token">The token.</param>
        /// <returns></returns>
        private static ConfigurationCollection ExtractValuesFromConfigurationAttributes(IEnumerable<BaseConfigurationAttribute> attrs, Type host, object context = null, CancellationToken token = default(CancellationToken))
        {
            ConfigurationCollection collection = new ConfigurationCollection();

            foreach (var attr in attrs.Reverse())
            {
                token.ThrowIfCancellationRequested();

                var values = attr.CollectConfigurationValue(host, context, token);
                token.ThrowIfCancellationRequested();

                foreach (var value in values)
                {
                    List<ConfigValue> configs = new List<Configuration.ConfigValue>();
                    if (collection.TryGetValue(value.Key, out configs))
                    {
                        foreach (var v in value.Value)
                        {
                            if (v.Override)
                            {
                                configs.Clear();
                            }
                            configs.Add(v);
                        }
                    }
                    else
                    {
                        collection.Add(value.Key, value.Value);
                    }
                }
            }

            return collection;
        }

        #endregion
    }
}
